/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.atomojo.tools.media.sync;

import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.atomojo.tools.media.MediaBrowser;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.FileRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.service.MetadataService;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *
 * @author alex
 */
public class DirectorySynchronizer {

   static final int BUFFER_SIZE = 8192;
   static final String ATOM_NS = "http://www.w3.org/2005/Atom";
   static final Logger LOG = Logger.getLogger(DirectorySynchronizer.class.getName());
   static DocumentBuilder docBuilder = null;
   static {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      try {
         docBuilder = factory.newDocumentBuilder();
      } catch (Exception ex) {
         LOG.log(Level.SEVERE,"Cannot configure document builder.",ex);
      }
   }
   
   public enum Mode {
      Upload,
      Download,
      UploadAndDownload
   }
   
   MediaBrowser browser;
   MetadataService metadataService;
   Mode mode;
   public DirectorySynchronizer(MediaBrowser browser) {
      this.browser = browser;
      this.metadataService = new MetadataService();
      metadataService.addCommonExtensions();
      this.mode = Mode.Upload;
   }
   
   public void setMode(Mode flag)
   {
      this.mode = flag;
   }
   
   public Mode getMode() {
      return this.mode;
   }
   
   public Logger getLogger() {
      return browser.getLogger();
   }
   
   public void sync(File dir, Reference service) {
      this.sync(dir,false, service);
   }
   public void sync(File dir, boolean newDir, Reference service) {
      switch (mode) {
         case Upload:
            uploadSync(dir,service);
            break;
         case Download:
            downloadSync(dir,service);
            break;
         case UploadAndDownload:
            if (newDir) {
               downloadSync(dir,service);
            } else {
               if (!uploadSync(dir,service)) {
                  downloadSync(dir,service);
               }
            }
            break;
      }
   }
   
   protected boolean uploadSync(File dir, Reference service) {
      boolean created = false;
      Reference collections = new Reference(service.toString()+"collections/");
      final Reference collection = new Reference(collections.toString());
      collection.addSegment(dir.getName());
      collection.setPath(collection.getPath()+"/");
      getLogger().info("Upload syncronizing "+dir.getAbsolutePath()+" against "+collection);
      getLogger().info("Checking collection: "+collection);
      Request headCollection = browser.newRequest(Method.HEAD, collection);
      Response headResult = browser.attemptCall(headCollection);
      if (headResult.getStatus().isError() && !headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
         getLogger().severe("Error "+headResult.getStatus()+" from server.");
         return created;
      } else if (headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
         getLogger().info("Creating collection...");
         if (!createCollection(dir.getName(),collections)) {
            return created;
         }
         created = true;
      }
      dir.listFiles(new FileFilter() {
         public boolean accept(File file) {
            if (file.isFile()) {
               Reference fileRef = new Reference(collection);
               fileRef.addSegment(file.getName());
               getLogger().info("Checking "+fileRef);
               Request headFile = browser.newRequest(Method.HEAD, fileRef);
               Response headResult = browser.attemptCall(headFile);
               if (headResult.getStatus().isSuccess()) {
                  Representation entity = headResult.getEntity();
                  if (entity!=null) {
                     Date lastModified = entity.getModificationDate();
                     if (lastModified==null) {
                        getLogger().warning("HEAD request did not return Last-Modified header.");
                     } else if (file.lastModified()>lastModified.getTime()) {
                        updateFile(file,fileRef);
                     }
                  } else {
                     getLogger().warning("HEAD request did not return entity for modification date.");
                  }
               } else if (headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
                  addFile(file,collection);
               } else {
                  getLogger().severe("Error checking status of "+file.getName()+", status "+headResult.getStatus());
               }
            }
            return false;
         }
      });
      return created;
   }
   protected void downloadSync(File dir, Reference service) {
      Reference collections = new Reference(service.toString()+"collections/");
      Reference collection = new Reference(collections.toString());
      collection.addSegment(dir.getName());
      collection.setPath(collection.getPath()+"/");
      getLogger().info("Download syncronizing "+dir.getAbsolutePath()+" against "+collection);
      getLogger().info("Checking collection: "+collection);
      Request getCollection = browser.newRequest(Method.GET, collection);
      Response getResult = browser.attemptCall(getCollection);
      if (!getResult.getStatus().isSuccess()) {
         if (getResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
            getLogger().severe("Collection "+collection+" does not exist.");
         } else {
            getLogger().severe("Cannot get collection "+collection+", status "+getResult.getStatus());
         }
         return;
      }
      if (!getResult.isEntityAvailable()) {
         getLogger().severe("Successfuly request on "+collection+" but entity is not available.");
         return;
      }
      Document feed = null;
      try {
         Reader r = getResult.getEntity().getReader();
         InputSource source = new InputSource(r);
         source.setSystemId(collection.toString());
         feed = docBuilder.parse(source);
      } catch (IOException ex) {
         getLogger().severe("I/O error while reading response.");
      } catch (SAXException ex) {
         getLogger().severe("Parse error while reading response: "+ex.getMessage());
      }
      NodeList entries = feed.getDocumentElement().getElementsByTagNameNS(ATOM_NS, "entry");
      URI collectionBase = URI.create(collection.toString());
      for (int i=0; i<entries.getLength(); i++) {
         Element entry = (Element)entries.item(i);
         NodeList contents = entry.getElementsByTagNameNS(ATOM_NS, "content");
         if (contents.getLength()>0) {
            Element content = (Element)contents.item(0);
            Attr src = content.getAttributeNode("src");
            if (src!=null) {
               String contentBase = content.getBaseURI();
               URI media = null;
               try {
                  URI base = contentBase==null ? collectionBase : new URI(contentBase);
                  media = base.resolve(src.getValue());
               } catch (URISyntaxException ex) {
                  getLogger().severe("Bad base URI "+contentBase+", "+ex.getMessage());
                  continue;
               }
               File file = null;
               int pos = media.getPath().lastIndexOf('/');
               if (pos<0) {
                  file = new File(dir,media.getPath());
               } else {
                  file = new File(dir,media.getPath().substring(pos+1));
               }
               Reference fileRef = new Reference(media.toString());
               getLogger().info("Checking "+fileRef);
               Request headFile = browser.newRequest(Method.HEAD, fileRef);
               Response headResult = browser.attemptCall(headFile);
               if (!headResult.getStatus().isSuccess()) {
                  if (headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
                     getLogger().warning(file.getAbsolutePath()+" is no longer on the server.");
                  } else {
                     getLogger().severe("Error checking status of "+fileRef+", status "+headResult.getStatus());
                  }
                  continue;
               }
               boolean outDated = !file.exists();
               if (file.exists()) {
                  Representation entity = headResult.getEntity();
                  if (entity==null) {
                     getLogger().severe("No Last-Modified header to check against local file for "+fileRef);
                     continue;
                  }
                  outDated = file.lastModified()<entity.getModificationDate().getTime();
                  if (outDated) {
                     getLogger().info("Out of date: "+file.lastModified()+" vs "+entity.getModificationDate().getTime());
                  }
               }
               if (outDated) {
                  downloadFile(file,fileRef);
               }
            }
         }
      }
   }
   
   protected void updateFile(File file,Reference fileRef) {
      getLogger().info("Updating "+file.getName());
      MediaType contentType = MediaType.APPLICATION_OCTET_STREAM;
      int pos = file.getName().lastIndexOf('.');
      if (pos>0) {
         String ext = file.getName().substring(pos+1).toLowerCase();
         contentType = metadataService.getMediaType(ext);
         if (contentType==null) {
            contentType = MediaType.APPLICATION_OCTET_STREAM;
         }
      }
      Request addFile = browser.newRequest(Method.PUT, fileRef);
      addFile.setEntity(new FileRepresentation(file,contentType));
      browser.addXLastModified(addFile, new Date(file.lastModified()));
      Response addResult = browser.attemptCall(addFile);
      if (!addResult.getStatus().isSuccess()) {
         getLogger().severe("Cannot update "+file.getName()+", status "+addResult.getStatus());
      }
   }
   
   protected void addFile(File file,Reference collection) {
      getLogger().info("Adding "+file.getName());
      MediaType contentType = MediaType.APPLICATION_OCTET_STREAM;
      int pos = file.getName().lastIndexOf('.');
      if (pos>0) {
         String ext = file.getName().substring(pos+1).toLowerCase();
         contentType = metadataService.getMediaType(ext);
         if (contentType==null) {
            contentType = MediaType.APPLICATION_OCTET_STREAM;
         }
      }
      Request addFile = browser.newRequest(Method.POST, collection);
      browser.addSlug(addFile, file.getName());
      addFile.setEntity(new FileRepresentation(file,contentType));
      browser.addXLastModified(addFile,new Date(file.lastModified()));
      Response addResult = browser.attemptCall(addFile);
      if (!addResult.getStatus().isSuccess()) {
         getLogger().severe("Cannot add "+file.getName()+", status "+addResult.getStatus());
      }
   }
   
   protected void downloadFile(File file,Reference fileRef) {
      getLogger().info("Downloading "+file.getName()+" to "+file.getAbsolutePath());
      Request getFile = browser.newRequest(Method.GET, fileRef);
      Response getResult = browser.attemptCall(getFile);
      if (!getResult.getStatus().isSuccess()) {
         getLogger().severe("Cannot get "+fileRef+", status "+getResult.getStatus());
         return;
      }
      if (!getResult.isEntityAvailable()) {
         getLogger().severe("Get on "+fileRef+" successful but entity is not available.");
         return;
      }
      InputStream is = null;
      OutputStream os = null;
      try {
         byte [] buffer = new byte[BUFFER_SIZE];
         int len;
         is = getResult.getEntity().getStream();
         os = new FileOutputStream(file);
         while ((len=is.read(buffer))>0) {
            os.write(buffer,0,len);
         }
      } catch (IOException ex) {
         getLogger().severe("I/O exception while downloading file "+fileRef);
      } finally {
         if (os!=null) {
            try {
               os.close();
            } catch (IOException ex) {
               getLogger().log(Level.SEVERE,"I/O error while closing output on "+file.getAbsolutePath());
            }
         }
         if (is!=null) {
            try {
               is.close();
            } catch (IOException ex) {
               getLogger().log(Level.SEVERE,"I/O error while closing input from "+fileRef);
            }
         }
      }
      Date lastModified = getResult.getEntity().getModificationDate();
      if (lastModified!=null) {
         file.setLastModified(lastModified.getTime());
      }
   }
   public boolean createCollection(String name,Reference collections) {
      getLogger().info("Posting new collection to "+collections);
      StringBuilder xml = new StringBuilder();
      xml.append("<app:collection xmlns:app=\"http://www.w3.org/2007/app\" xmlns:atom=\"http://www.w3.org/2005/Atom\"><atom:title>");
      xml.append(name.replace("&", "&amp;").replace("<","&lt;"));
      xml.append("</atom:title></app:collection>");
      Request createRequest = browser.newRequest(Method.POST, collections);
      browser.addSlug(createRequest,name);
      createRequest.setEntity(new StringRepresentation(xml.toString(),MediaType.valueOf("application/atomsvc+xml;type=collection")));
      Response createResponse = browser.attemptCall(createRequest);
      if (createResponse.getStatus().isError()) {
         getLogger().severe("Cannot create collection, status "+createResponse.getStatus());
      }
      return createResponse.getStatus().isSuccess();
   }
}
